﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Objects;
using System.ComponentModel;
using System.Globalization;
using System.Data.Objects.DataClasses;
using System.Data;

namespace SystemHelperLibrary.EntityFramework
{
	/// <summary>
	/// When using LINQ to Entities, queries are instances of ObjectQuery but are statically
	/// typed as IQueryable. This makes it difficult to access members of ObjectQuery, particularly
	/// when the element type is anonymous. These extensions make it easier to access ObjectQuery
	/// members.
	/// </summary>
	/// <example>
	/// <code>
	/// var q = from p in context.Products
	///         where p.ProductName.StartsWith("Foo")
	///         select p;
	/// 
	/// // before
	/// string commandText = ((ObjectQuery&lt;Product&gt;)q).ToTraceString();
	/// 
	/// // after
	/// string commandText = q.ToTraceString();
	/// </code>
	/// </example>
	public static class ObjectQueryExtensions
	{
		/// <summary>
		/// Returns the given IQuerable instance as an ObjectQuery instance.
		/// </summary>
		/// <typeparam name="T">Element type.</typeparam>
		/// <param name="source">IQueryable instance.</param>
		/// <returns>source as an ObjectQuery</returns>
		public static ObjectQuery<T> AsObjectQuery<T>(this IQueryable<T> source)
		{
			return source as ObjectQuery<T>;
		}

		/// <summary>
		/// Retrieves Entity Framework trace information for the given query.
		/// </summary>
		/// <typeparam name="T">Element type.</typeparam>
		/// <param name="source">IQueryable instance. Must be an ObjectQuery instance at runtime.</param>
		/// <returns>Trace string for the query.</returns>
		public static string ToTraceString<T>(this IQueryable<T> source)
		{
			return source.ToObjectQuery("source").ToTraceString();
		}

		/// <summary>
		/// Includes navigation path in query result.
		/// </summary>
		/// <typeparam name="T">Element type.</typeparam>
		/// <param name="source">IQueryable instance. Must be an ObjectQuery instance at runtime.</param>
		/// <param name="path">Navigation path.</param>
		/// <returns>Query with spanned navigations.</returns>
		public static IQueryable<T> Include<T>(this IQueryable<T> source, string path)
		{
			return source.ToObjectQuery("source").Include(path);
		}

		/// <summary>
		/// Sets merge option for a query.
		/// </summary>
		/// <typeparam name="T">Element type.</typeparam>
		/// <param name="source">IQueryable instance. Must be an ObjectQuery instance at runtime.</param>
		/// <param name="mergeOption">Merge option to use when evaluating the query.</param>
		/// <returns>Query with merge option set.</returns>
		public static IQueryable<T> SetMergeOption<T>(this IQueryable<T> source, MergeOption mergeOption)
		{
			ObjectQuery<T> result = source.ToObjectQuery("source");
			result.MergeOption = mergeOption;
			return result;
		}

		/// <summary>
		/// Returns binding list for the given query instance.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="source"></param>
		/// <returns></returns>
		public static IBindingList ToBindingList<T>(this IQueryable<T> source)
		{
			if (source == null)
				throw new ArgumentNullException("source");

			IListSource listSource = source as IListSource;
			if (null == listSource)
			{
				throw new ArgumentException("Unable to get BindingList", "source");
			}
	
			IBindingList bindingList = listSource.GetList() as IBindingList;
			
			if (null == bindingList)
			{
				throw new ArgumentException("Unable to get BindingList", "source");
			}
			return bindingList;
		}

		private static ObjectQuery<T> ToObjectQuery<T>(this IQueryable<T> source, string argumentName)
		{
			if (source == null)
				throw new ArgumentNullException("source");

			ObjectQuery<T> result = source as ObjectQuery<T>;
			if (null == result)
			{
				throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "Operation Requires ObjectQuery", argumentName));
			}
			return result;
		}
	}

	/// <summary>
	/// Extensions supporting easy retrieval and setting of reference key values. Currently, the EntityReference
	/// class requires metadata for a key but does not directly expose that metadata. This class exposes the 
	/// keys as pure values.
	/// </summary>
	/// <example>
	/// <code>
	/// // existing pattern
	/// product.CategoryReference.EntityKey = new EntityKey("EntityContainer.Categories", new EntityKeyMember("CategoryID", 1), new EntityKeyMember("DivisionID", 1))); 
	/// 
	/// // new pattern
	/// product.CategoryReference.SetKey(1, 1);
	/// </code>
	/// </example>
	public static class EntityReferenceExtensions
	{
		/// <summary>
		/// Gets key value for a non-compound reference key (e.g. one foreign key component).
		/// </summary>
		/// <typeparam name="T">EntityReference element type</typeparam>
		/// <param name="entityReference">Entity reference.</param>
		/// <returns>Key value from entity reference.</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0")]
		public static object GetKey<T>(this EntityReference<T> entityReference)
			where T : class, IEntityWithRelationships
		{			
			EntityKey entityKey = entityReference.EntityKey;
			if (null == entityKey)
			{
				if (entityReference.GetTargetEntitySet().ElementType.KeyMembers.Count != 1)
				{
					throw new InvalidOperationException("Simple Key  Only");
				}
				return null;
			}
			
			var entityKeyValues = entityKey.EntityKeyValues;
			if (entityKeyValues.Length != 1)
			{
				throw new InvalidOperationException("Simple Key  Only");
			}
			return entityKeyValues[0].Value;
		}

		/// <summary>
		/// Gets a component of a key value for a (potentially compound) reference key.
		/// </summary>
		/// <typeparam name="T">EntityReference element type</typeparam>
		/// <param name="entityReference">Entity reference.</param>
		/// <param name="keyOrdinal">Index of the key component (with respect to the element type's
		/// EntityType.KeyMembers).</param>
		/// <returns>Key component value from entity reference.</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0")]
		public static object GetKey<T>(this EntityReference<T> entityReference, int keyOrdinal)
			where T : class, IEntityWithRelationships
		{
			if (keyOrdinal < 0)
			{
				throw new ArgumentOutOfRangeException("keyOrdinal");
			}
			EntityKey entityKey = entityReference.EntityKey;
			if (null == entityKey)
			{
				if (entityReference.GetTargetEntitySet().ElementType.KeyMembers.Count <= keyOrdinal)
				{
					throw new ArgumentOutOfRangeException("keyOrdinal");
				}
				return null;
			}
			if (entityKey.EntityKeyValues.Length <= keyOrdinal)
			{
				throw new ArgumentOutOfRangeException("keyOrdinal");
			}
			return entityKey.EntityKeyValues[keyOrdinal].Value;
		}

		/// <summary>
		/// Sets reference key values.
		/// </summary>
		/// <typeparam name="T">EntityReference element type</typeparam>
		/// <param name="entityReference">Entity reference.</param>
		/// <param name="keyValues">Components of the key (aligned with the element type EntityType.KeyMembers)</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0")]
		public static void SetKey<T>(this EntityReference<T> entityReference, params object[] keyValues)
			where T : class, IEntityWithRelationships
		{
			// if null keyValues given, clear the entity key
			if (null == keyValues)
			{
				entityReference.EntityKey = null;
			}

			IEnumerable<string> keyComponentNames;
			int expectedKeyComponentCount;
			string entitySetName;

			if (null == entityReference.EntityKey)
			{
				// if there is no existing key, retrieve metadata through reflection
				System.Data.Metadata.Edm.EntitySet targetEntitySet = entityReference.GetTargetEntitySet();
				keyComponentNames = targetEntitySet.ElementType.KeyMembers.Select(m => m.Name);
				expectedKeyComponentCount = targetEntitySet.ElementType.KeyMembers.Count;
				entitySetName = targetEntitySet.EntityContainer.Name + "." + targetEntitySet.Name;
			}
			else
			{
				// if there is an existing key, just borrow its metadata
				EntityKey existingKey = entityReference.EntityKey;
				keyComponentNames = existingKey.EntityKeyValues.Select(v => v.Key);
				expectedKeyComponentCount = existingKey.EntityKeyValues.Length;
				entitySetName = existingKey.EntityContainerName + "." + existingKey.EntitySetName;
			}

			// check that the correct number of key values is given
			if (keyValues != null && expectedKeyComponentCount != keyValues.Length)
			{
				throw new ArgumentException( "keyValues");
			}

			// check if there are any null key components (if so, the entire key is assumed
			// to be null)
			if (keyValues == null || keyValues.Any(v => null == v))
			{
				entityReference.EntityKey = null;
			}
			else
			{
				// create a new entity key with the given key component names and key component values
				EntityKey entityKey = new EntityKey(entitySetName, keyComponentNames.Zip(keyValues,
					(name, value) => new EntityKeyMember(name, value)));
				entityReference.EntityKey = entityKey;
			}
		}

		/// <summary>
		/// Uses internal API to access metadata for related end target.
		/// </summary>
		/// <param name="relatedEnd">Related end.</param>
		/// <returns>Entity set targeted by the related end.</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0")]
		public static System.Data.Metadata.Edm.EntitySet GetTargetEntitySet(this RelatedEnd relatedEnd)
		{
			System.Data.Metadata.Edm.AssociationSet associationSet = (System.Data.Metadata.Edm.AssociationSet)relatedEnd.RelationshipSet;

			if (null == associationSet)
			{
				throw new InvalidOperationException("Cannot determine metadata for related end");
			}

			return associationSet.AssociationSetEnds[relatedEnd.TargetRoleName].EntitySet;
		}
	}
}
